// Copyright 2022 Huawei Cloud Computing Technology Co., Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import work from 'webworkify-webpack';
import Subscribe from './common/Subscribe';
import Util from './Util';

const SAMPLE_RATE = 48000;
const MAX_VOLUME_VALUE = 100;
const DEFAULT_VALUME_VALUE = 50;
const DEFAULT_CACHE_LENGTH = 10; // 音频缓存长度默认为 10，即 100ms 的音频缓存
const MAX_PLAY_DELAY_S = 0.4; //播放最大时延，单位秒

class AudioPlayer {
    constructor({channels, videoPlayer, volume}) {
        this.channels = channels || 1;
        this.playerTimer = null;
        this.penddingBuffer = new Float32Array();
        this.nextTime = 0;
        this._volume = volume || DEFAULT_VALUME_VALUE;
        this.cacheLength = 0;
        this.util = new Util();

        this.audioDecoderWorker = work(require.resolve('./worker/AudioDecoder.js'));
        this.audioDecoderWorker.addEventListener('message', this.messageHandler.bind(this));

        window.AudioContext = window.AudioContext || window.webkitAudioContext;
        this.context = new AudioContext();
        if (this.context.createGain) {
            this.gainNode = this.context.createGain();
            this.gainNode && (this.gainNode.gain.value = this._volume / MAX_VOLUME_VALUE);
        }

        this.subscribe = new Subscribe(['audioStateChange']);
        this.util.bind(this.context, 'statechange', () => {
            this.subscribe.trigger('audioStateChange', this.context.state);
        });
        this.videoPlayer = videoPlayer;
        // iOS和chrome上需用户操作里 resume 才能播放音频，不然 audioContext 一直 suspend
        if (this.videoPlayer) {
            this.util.bind(this.videoPlayer, 'touchstart', this.resume.bind(this));
            this.util.bind(this.videoPlayer, 'mousedown', this.resume.bind(this));
        }
    }

    initAfterSubscribe(eventName) {
        if (eventName === 'audioStateChange' && this.context) {
            this.subscribe.trigger('audioStateChange', this.context.state);
        }
    }

    /**
     * 监听事件，如监听audioStateChange事件
     * @param {string} eventName 事件名称
     * @param {object} callback 回调函数
     **/
    on(eventName, callback) {
        this.subscribe.on(eventName, callback);
        this.initAfterSubscribe(eventName);
    }

    /**
     * 解除事件监听
     * @param {string} eventName 事件名称
     * @param {object} callback 回调函数
     **/
    off(eventName, callback) {
        this.subscribe.off(eventName, callback);
    }

    getVolume() {
        return this._volume;
    }

    // 音量设置范围为 0 ~ 100
    setVolume(value) {
        this._volume = value;

        if (this.gainNode) {
            this.gainNode.gain.value = value / MAX_VOLUME_VALUE;
        }
    }

    messageHandler(evt) {
        const message = evt.data;
        if (!Object.prototype.hasOwnProperty.call(message, 'type')) {
            return;
        }

        switch (message.type) {
            case 'decodeResult': {
                this.cacheLength++;

                const decodeBuffer = message.data;
                const pcmTmp = new Float32Array(decodeBuffer);
                const tmp = new Float32Array(this.penddingBuffer.length + pcmTmp.length);
                tmp.set(this.penddingBuffer, 0);
                tmp.set(pcmTmp, this.penddingBuffer.length);
                this.penddingBuffer = null;
                this.penddingBuffer = tmp;

                if (this.cacheLength >= DEFAULT_CACHE_LENGTH) {
                    this.play();
                }

                break;
            }

            default:
                // do nothing
        }
    }

    resume() {
        if (this.context.state !== 'running') {
            this.context.resume();
        }

        if (this.context.state === 'running') {
            this.util.unbind(this.videoPlayer, 'touchstart');
            this.util.unbind(this.videoPlayer, 'mousedown');
        }
    }

    feed(data) {
        this.audioDecoderWorker.postMessage({
            type: 'decode',
            data: data.buffer
        }, [data.buffer]);
    }

    play() {
        if (!this.penddingBuffer.length) {
            return;
        }

        // audio播放器状态未ready且还未开始播放的情况下，丢掉audio数据，规避iOS下audio播放延迟问题（用户点击后才开始播放，且从头开始播放，故有延迟）；超时最大时延将丢掉。
        if ((this.context.state !== 'running' && !this.context.currentTime) || (this.nextTime - (this.context.currentTime || 0)) > MAX_PLAY_DELAY_S) {
            this.penddingBuffer = new Float32Array();
            return;
        }

        this.cacheLength = 0;
        const channels = this.channels;
        let frameCount = Math.floor(this.penddingBuffer.length / channels);
        let source = this.context.createBufferSource();
        let audioBuffer = this.context.createBuffer(channels, frameCount, SAMPLE_RATE);
        let fadeCount = Math.min(frameCount / 2, 100);
        for (let channelCount = 0; channelCount < channels; channelCount++) {
            let channelData = audioBuffer.getChannelData(channelCount);
            for (let i = 0; i < frameCount; i++) {
                channelData[i] = this.penddingBuffer[i * channels + channelCount];

                if (i < fadeCount) {
                    channelData[i] = channelData[i] * i / fadeCount; // fade in
                }

                if (i >= (frameCount - fadeCount)) {
                    channelData[i] = channelData[i] * (frameCount - i - 1) / fadeCount; // fade out
                }
            }
        }

        source.buffer = audioBuffer;
        if (this.gainNode) {
            source.connect(this.gainNode);
            this.gainNode.connect(this.context.destination);
        } else {
            source.connect(this.context.destination);
        }

        this.nextTime = Math.max(this.nextTime, this.context.currentTime);
        source.start(this.nextTime);
        this.nextTime += source.buffer.duration;
        this.penddingBuffer = new Float32Array();
    }

    destroy() {
        this.playerTimer && clearInterval(this.playerTimer);
        this.audioDecoderWorker.terminate();
        this.util.unbind(this.videoPlayer, 'touchstart');
        this.util.unbind(this.videoPlayer, 'mousedown');
        this.util.unbind(this.context, 'statechange');
        this.context && this.context.close();
        this.videoPlayer = null;
        this.context = null;
    }
}

export default AudioPlayer;
